package org.hackystat.projecttrajectory.resource.estimate;

import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.hackystat.projecttrajectory.db.DbManager;
import org.hackystat.projecttrajectory.resource.estimate.jaxb.EstimateData;
import org.hackystat.projecttrajectory.resource.estimate.jaxb.EstimateIndex;
import org.hackystat.projecttrajectory.resource.estimate.jaxb.EstimateRef;
import org.hackystat.projecttrajectory.resource.estimate.jaxb.ProjectEstimate;
import org.hackystat.projecttrajectory.server.Server;
import org.hackystat.sensorbase.resource.projects.jaxb.Project;
import org.hackystat.sensorbase.resource.users.jaxb.User;
import org.hackystat.utilities.stacktrace.StackTrace;
import org.hackystat.utilities.tstamp.Tstamp;
import org.w3c.dom.Document;

/**
 * Provides a manager for the Estimate Resource.
 * 
 * @author Cam Moore
 * 
 */
public class EstimateManager {

  /** Holds the class-wide JAXBContext, which is thread-safe. */
  private JAXBContext jaxbContext;

  /** The Server associated with this EstimateManager. */
  private Server server;

  /** The DbManager associated with this server. */
  private DbManager dbManager;

  /** The initial size for Collection instances that hold the ProjectEstimates. */
  private static final int estimateSetSize = 127;

  /** The in memory cache of ProjectEstimates keyed by user and project name. */
  private Map<String, Map<String, ProjectEstimate>> user2project2estimate 
  = new HashMap<String, Map<String, ProjectEstimate>>(
      estimateSetSize);

  /**
   * The in-memory repository of ProjectEstimate XML strings, keyed by
   * ProjectEstimate.
   */
  private EstimateStringMap estimate2xml = new EstimateStringMap();

  /**
   * The in-memory repository of ProjectEstimateRef XML strings, keyed by
   * ProjectEstimate.
   */
  // private EstimateStringMap estimate2Ref = new EstimateStringMap();
  /**
   * Constructs a EstimateManager. There is one EstimateManager per server.
   * 
   * @param server The server instance.
   */
  public EstimateManager(Server server) {
    this.server = server;
    this.dbManager = (DbManager) server.getContext().getAttributes().get("DbManager");
    try {
      this.jaxbContext = JAXBContext
          .newInstance("org.hackystat.projecttrajectory.resource.estimate.jaxb");
      initializeCache(); // NOPMD
    } catch (Exception e) {
      String msg = "Exception during ProjectManager initialization processing";
      server.getLogger().warning(msg + "\n" + StackTrace.toString(e));
      throw new RuntimeException(msg, e);
    }
  }

  /** Read in all Estimates from the database and initialize the in-memory cache. */
  @SuppressWarnings("unchecked")
  private final void initializeCache() {
    try {
      EstimateIndex index = makeEstimateIndex(this.dbManager.getProjectEstimateIndex());

      for (EstimateRef ref : index.getEstimateRef()) {
        String estimator = ref.getEstimator();
        // Check to make sure user exists. DB is not normalized!
        if (estimator == null) {
          String msg = "ProjectEstimate with undefined null user found while "
              + "initializing estimate cache from database. ProjectEstimate will be ignored.";
          server.getLogger().fine(msg);
        } else {
          String projectName = ref.getProject();
          if (this.hasEstimates(estimator, projectName)) { 
            String msg = "Duplicate project estimates by " + estimator + " for project " 
            + projectName + " found in database. Ignoring.";
            server.getLogger().warning(msg);
          } else {
            String xmlString = this.dbManager.getProjectEstimate(estimator, projectName);
            this.updateCache(makeProjectEstimate(xmlString), xmlString);
          }
        }
      }
    } catch (Exception e) {
      server.getLogger().warning("Failed to initialize estimates " + StackTrace.toString(e));
    }
  }

  /**
   * Updates the cache given all the Project representations. Throws unchecked
   * exceptions if the Owner is not defined as a User.
   * 
   * @param estimate The ProjectEstimate.
   * @param estimateXml The ProjectEstimate as an XML string.
   */
  @SuppressWarnings("unchecked")
  private final void updateCache(ProjectEstimate estimate, String estimateXml) {
    try {
      String estimator = estimate.getEstimator();
      if (estimator == null) {
        throw new IllegalArgumentException("ProjectEstimate with undefined estimator"); 
      }
      if (!user2project2estimate.containsKey(estimator)) {
        user2project2estimate.put(estimator, new HashMap<String, ProjectEstimate>());
      }
      user2project2estimate.get(estimator).put(estimate.getProject(), estimate);
      estimate2xml.put(estimate, estimateXml);
    } catch (Exception e) {
      server.getLogger().warning("Failed to update cache " + StackTrace.toString(e));
    }

  }

  
  /**
   * @return An XML String representing the EstimateIndex for all the estimates.
   */
  public synchronized String getEstimateIndex() {
    // !!! Need to get the data from the cache instead of the database.
    return this.dbManager.getProjectEstimateIndex();
  }
  /**
   * Returns the XML string containing the EstimateIndex with all Project
   * estimates. Uses the in-memory cache of EstimateRef strings.
   * 
   * @param user the user associated with the estimate.
   * @return The XML string providing an index to all current Projects.
   */
  public synchronized String getEstimateIndex(User user) {
    return this.dbManager.getEstimateIndex(user);
  }
  
  /**
   * Returns the XML string containing the EstimateIndex with all Project
   * estimates. Uses the in-memory cache of EstimateRef strings.
   * 
   * @param user the user associated with the estimate.
   * @return The XML string providing an index to all current Projects.
   */
  public synchronized String getUserEstimateIndex(String user) {
    return this.dbManager.getUserEstimateIndex(user);
  }

  /**
   * Returns the EstimateIndex as an XML String for all the estimates for the given project.
   * @param project the project.
   * @return An XML String representing the EstimateIndex for the given project.
   */
  public synchronized String getProjectEstimateIndex(Project project) {
    return this.dbManager.getProjectEstimateIndex(project);
  }

  
  /**
   * Returns the EstimateIndex as an XML String for all the estimates for the given project.
   * @param project the project.
   * @return An XML String representing the EstimateIndex for the given project.
   */
  public synchronized String getProjectEstimateIndex(String project) {
    return this.dbManager.getProjectEstimateIndex(project);
  }

  /**
   * Returns the XML string containing the ProjectEstimate for the given user and project.
   * 
   * @param user the user associated with the estimate.
   * @param project the project.
   * @return The XML string providing an index to all current Projects.
   */
  public synchronized String getEstimate(User user, Project project) {
    if (user2project2estimate.get(user.getEmail()) != null) {
      ProjectEstimate estimate = user2project2estimate.get(user.getEmail()).get(project.getName());
      if (estimate != null) {
        return estimate2xml.get(estimate);
      }
    }
    // !!! need to use the cache instead of the database.
    return this.dbManager.getProjectEstimate(user, project);
  }
  
  /**
   * Returns the XML string containing the ProjectEstimate for the given user and project.
   * 
   * @param user the user associated with the estimate.
   * @param project the project.
   * @return The XML string providing an index to all current Projects.
   */
  public synchronized String getEstimate(String user, String project) {
    if (user2project2estimate.get(user) != null) {
      ProjectEstimate estimate = user2project2estimate.get(user).get(project);
      if (estimate != null) {
        return estimate2xml.get(estimate);
      }
    }
    // !!! need to use the cache instead of the database.
    return this.dbManager.getProjectEstimate(user, project);    
  }

  /**
   * Updates the manager with the given estimate. A new estimate is added to the
   * estimates.
   * 
   * @param estimate the project estimate.
   */
  public synchronized void putEstimate(ProjectEstimate estimate) {
    try {
      estimate.setLastModDate(Tstamp.makeTimestamp());
      String xmlProjectEstimate = makeProjectEstimate(estimate);
      List<EstimateData> dataList = estimate.getEstimateData();
      for (EstimateData data : dataList) {
        String xmlEstimateRef = makeEstimateRef(estimate.getEstimator(), 
            estimate.getProject(), data);         
        dbManager.storeProjectEstimate(estimate, xmlProjectEstimate, xmlEstimateRef);
      }
      updateCache(estimate, xmlProjectEstimate);
    } catch (Exception e) {
      e.printStackTrace();
      server.getLogger().warning("Failed to add estimate for " + estimate.getProject() + " " +
          StackTrace.toString(e));
    }
  }

  /**
   * Creates the XML String EstimateRef for the given estimator, project and EstimateData.
   * @param estimator The person who made the estimate.
   * @param project The project.
   * @param data the actual estimate data.
   * @return The XML String representation of the EstimageRef.
   */
  private String makeEstimateRef(String estimator, String project, EstimateData data) {
    StringBuffer buff = new StringBuffer(128);
    buff.append("<EstimateRef Project=\"");
    buff.append(project);
    buff.append("\" Estimator=\"");
    buff.append(estimator);
    buff.append("\" href=\"");
    buff.append(this.server.getHostName());
    buff.append('/');
    buff.append(estimator);
    buff.append('/');
    buff.append(project);
    buff.append("\" LastMod=\"");
    buff.append(data.getEstimateDate());
    buff.append("\"/>");
    return buff.toString();
  }

  /**
   * Ensures the given estimate is no longer in this Manager. There may be other
   * estimates for the project.
   * 
   * @param estimate the estimate to remove.
   */
  public synchronized void deleteEstimate(ProjectEstimate estimate) {
    // do stuff
  }

  /**
   * Returns the ProjectEstimate XML associated with the given user and project.
   * 
   * @param user the user.
   * @param projectName the name of the project.
   * @return The ProjectEstimate XML String, or null if not found.
   */
  public synchronized String getEstimateString(String user, String projectName) {
    if (!user2project2estimate.containsKey(user)) {
      // no user in cache so return null
      return null;
    }
    ProjectEstimate estimate = user2project2estimate.get(user).get(projectName);
    try {
      return makeProjectEstimate(estimate);
    } catch (Exception e) {
      server.getLogger().warning("Error translating ProjectEstimate " + StackTrace.toString(e));
      return null;
    }
  }

  /**
   * Returns true if the passed estimator and project has any estimates.
   * 
   * @param estimator the estimator's name.
   * @param project the project name.
   * @return true if the project has any estimates, false otherwise.
   */
  public synchronized boolean hasEstimates(String estimator, String project) {
    if (user2project2estimate.get(estimator) != null) {
      return user2project2estimate.get(estimator).get(project) != null;
    }
    return false;
  }

  /**
   * Takes a String encoding of a ProjectEstimate in XML format and converts it
   * to an instance.
   * 
   * @param xmlString The XML string representing a ProjectEstimate.
   * @return The corresponding ProjectEstimate instance.
   * @throws Exception If problems occur during unmarshalling.
   */
  public synchronized ProjectEstimate makeProjectEstimate(String xmlString) throws Exception {
    Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
    return (ProjectEstimate) unmarshaller.unmarshal(new StringReader(xmlString));
  }

  /**
   * Takes the String encoding of an EstimateIndex and converts it to an
   * instance.
   * 
   * @param xmlString The xml representation.
   * @return An EstimateIndex instance.
   * @throws Exception If problems occur during unmarshalling.
   */
  public synchronized EstimateIndex makeEstimateIndex(String xmlString) throws Exception {
    Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
    return (EstimateIndex) unmarshaller.unmarshal(new StringReader(xmlString));
  }

  /**
   * Returns the XML String representation of the passed ProjectEstimate.
   * 
   * @param estimate the ProjectEstimate.
   * @return The XML String representation of the estimate.
   * @throws Exception if problems occur during translation.
   */
  public synchronized String makeProjectEstimate(ProjectEstimate estimate) throws Exception {
    Marshaller marshaller = jaxbContext.createMarshaller();
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    dbf.setNamespaceAware(true);
    DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
    Document doc = documentBuilder.newDocument();
    marshaller.marshal(estimate, doc);
    DOMSource domSource = new DOMSource(doc);
    StringWriter writer = new StringWriter();
    StreamResult result = new StreamResult(writer);
    TransformerFactory tf = TransformerFactory.newInstance();
    Transformer transformer = tf.newTransformer();
    transformer.transform(domSource, result);
    String xmlString = writer.toString();
    // Now remove the processing instruction.  This approach seems like a total hack.
    xmlString = xmlString.substring(xmlString.indexOf('>') + 1);
    return xmlString;
  }

}
